# -*- coding: utf-8 -*-

"""
common utilities for rdf based data model
"""

from __future__ import unicode_literals
from __future__ import absolute_import
from __future__ import division

import six
import json
from typing import Dict, Text, List
from kgpipeline.cnschema import KgCnSchema

# standard prefixes
RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
RDFS = "http://www.w3.org/2000/01/rdf-schema#"
OWL = "http://www.w3.org/2002/07/owl#"
XSD = "http://www.w3.org/2001/XMLSchema#"
CNS = "http://cnschema.org/"


def prefix_iri(prefix, name):
    return "<{0}{1}>".format(prefix, name)


def rdf(name):
    return prefix_iri(RDF, name)


def rdfs(name):
    return prefix_iri(RDFS, name)


def cns(name):
    return prefix_iri(CNS, name)


def xsd(name):
    return prefix_iri(XSD, name)


A_TYPE = rdf("type")
CLASS = rdfs("Class")
PROPERTY = rdf("Property")
SUB_CLASS_OF = rdfs("subClassOf")
SUB_PROPERTY_OF = rdfs("subPropertyOf")
DOMAIN = rdfs("domain")
RANGE = rdfs("range")

# cnSchema types ==> xsd data types
DATA_TYPES = {
    "boolean": xsd("boolean"),
    "text": xsd("string"),
    "date": xsd("date"),
    "datetime": xsd("dateTime"),
    "number": xsd("decimal"),
    "url": xsd("anyURI"),
    "integer": xsd("integer"),
    "float": xsd("double"),
    "decimal": xsd("decimal")
}

CNS_PROPERTIES = {"@context", "@vocab", "@graph", "@id", "@type", "graphId"}


def value_to_literal(value, value_type):
    if isinstance(value, six.string_types):
        res = value
    elif isinstance(value, bool):
        res = "true" if value else "false"
    else:
        res = str(value)

    # dump and escape a string literal
    # should be the same as N-Quads literal
    res = json.dumps(res, ensure_ascii=False)

    if value_type == "text":
        return res
    else:
        literal_type = DATA_TYPES.get(value_type)
        if literal_type:
            return "{0}^^{1}".format(res, literal_type)
        else:
            return res


def quad(s, p, o, g=None):
    if g:
        return "{0}\t{1}\t{2}\t{3}\t.".format(s, p, o, g)
    else:
        return "{0}\t{1}\t{2}\t.".format(s, p, o)


def cns_schema_to_nquads(schema):  # type: (KgCnSchema) -> List[Text]
    """
    for a cnSchema ontology, we generate following additional metadata:
        1. type hierarchies (rdfs:subClassOf, rdfs:subPropertyOf)
        2. property (link) range and domain (rdfs:range, rdfs:domain)
        3. cardinality and range of a particular binding (template)
           (owl:Restriction owl:onProperty owl:minCardinality owl:maxCardinality owl:allValuesFrom)

    special cases:
        1. for a link defined as a class, we need the template information for in and out ranges
           (as domain and range of it when used as property)
    """
    res = []
    # type hierarchy, only entity space (Thing and CnsLink derived)
    # we want Thing a rdfs:Class, CnsLink a rdfs:Property
    for t, th in six.iteritems(schema.types):
        if "Thing" in th:
            s = cns(th[0])
            res.append(quad(s, A_TYPE, CLASS))
            if len(th) > 1:
                o = cns(th[1])
                res.append(quad(s, SUB_CLASS_OF, o))

        elif "CnsLink" in th:
            s = cns(th[0])
            res.append(quad(s, A_TYPE, PROPERTY))
            if len(th) > 1:
                o = cns(th[1])
                res.append(quad(s, SUB_PROPERTY_OF, o))

            props = schema.plist.get(t)
            if props:
                if "in" in props:
                    link_domain = props["in"]["range"]
                    res.append(quad(s, DOMAIN, cns(link_domain)))
                if "out" in props:
                    link_range = props["out"]["range"]
                    res.append(quad(s, RANGE, cns(link_range)))

    return res


def cns_item_to_nquads(schema, item, default_graph_id):  # type: (KgCnSchema, Dict, Text) -> List[Text]
    res = []

    if "graphId" in item:
        graph_id = item["graphId"]
    else:
        graph_id = default_graph_id

    def iri_by_id(name):
        return cns("graph/{0}/{1}".format(graph_id, name))

    # @id property
    subject_iri = iri_by_id(item["@id"])
    types = item["@type"]

    is_link = "CnsLink" in types
    if is_link:
        in_iri = iri_by_id(item["in"])
        out_iri = iri_by_id(item["out"])

    # @type property
    main_types = schema.get_main_types(types)

    for main_type in main_types:
        main_type_iri = cns(main_type)

        # subject rdf:type main_type
        res.append(quad(subject_iri, A_TYPE, main_type_iri))

        # for links, we generate additional quad for easier SPARQL queries
        # it is possible that we generate duplicate entries
        # for example, multiple graph will have several links of same type,
        # we need to distinct the result in the final step
        if is_link:
            res.append(quad(in_iri, main_type_iri, out_iri))

    keys = set(item.keys()) - CNS_PROPERTIES

    blank_node_id = 1

    for key in keys:
        # subject <IRI> value
        # we then process the value as a string, with proper xsd data type postfix
        property_iri = cns(key)
        key_definition = schema.get_definition(key)
        if key_definition:
            intended_range = key_definition.get("range")
            raw_value = item[key]

            if isinstance(raw_value, list):
                for value in raw_value:
                    res.append(quad(subject_iri, property_iri, value_to_literal(value, intended_range)))
            elif isinstance(raw_value, dict):
                # structure value
                # we use a blank node to denote a DataStructure value
                sv_iri = "_:{0}.{1}".format(item["@id"], blank_node_id)
                blank_node_id += 1
                res.append(quad(subject_iri, property_iri, sv_iri))
                for k, v in six.iteritems(raw_value):
                    sv_property_iri = cns(key)
                    sv_property_range = schema.get_definition(key).get("range")
                    res.append(quad(sv_iri, sv_property_iri, value_to_literal(v, sv_property_range)))
            else:
                res.append(quad(subject_iri, property_iri, value_to_literal(raw_value, intended_range)))

    return res
